<?php

/**
 * SvcPHP
 *
 * Copyright (C) 2012 João Francisco Biondo Trinca
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge,
 * publish, distribute, sublicense, and/or sell copies of the Software,
 * and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 *
 * @package    SvcPHP
 * @author     Stepan Kravchenko <stepan.krab@gmail.com>
 * @license    http://www.opensource.org/licenses/mit-license.html  MIT License
 * @link       http://code.google.com/p/yii-razor-view-render/
 *             http://code.google.com/p/svcphp/
 *
 */

/**
 * Implements the template context class.
 */
class Svc_Template_Renderer
{
    /**
     * Stores the input contents.
     *
     * @var string
     */
    private $_input;

    /**
     * Stores the final output.
     *
     * @var string
     */
    private $_output;

    /**
     * Stores the source file for exception messages.
     *
     * @var string
     */
    private $_file;

    /**
     * Creates a template renderer instance.
     *
     * @param mixed $content
     *
     * @return Svc_TemplateRenderer
     */
    public function __construct($content, $file)
    {
        $this->_file = $file;
        $this->_input = $content;
        $this->_output = "<?php if (!class_exists('Svc_Template')) { exit(0); } ?>\n";
    }

    /**
     * Generates the final PHP output.
     *
     * @return string
     */
    public function generate()
    {
        $this->parse(0, strlen($this->_input));

        return $this->_output;
    }

    /**
     * Parse block of input template file from $beginBlock position to ($endBlock - 1).
     * Replaced all valid @-statement of Razor template syntax.
     *
     * @param int $beginBlock First symbol of block to parse.
     * @param int $endBlock The position after last symbol of block to parse.
     */
    private function parse($beginBlock, $endBlock)
    {
        $offset = $beginBlock;
        while (($p = strpos($this->_input, "@", $offset)) !== false && $p < $endBlock)
        {
            // replace @@ -> @
            if ($this->isNextToken($p, $endBlock, "@"))
            {
                $this->_output .= substr($this->_input, $offset, $p - $offset + 1);
                $offset = $p + 2;
                continue;
            }

            // replace multi-token statements @(...)
            if ($this->isNextToken($p, $endBlock, "("))
            {
                $end = $this->findClosingBracket($p + 1, $endBlock, "(", ")");
                $this->_output .= substr($this->_input, $offset, $p - $offset);
                $this->generateOutput($p, $end);
                $offset = $end + 1;
                continue;
            }

            // replace multi-line statements @{...}
            if ($this->isNextToken($p, $endBlock, "{"))
            {
                $end = $this->findClosingBracket($p + 1, $endBlock, "{", "}");
                $this->_output .= substr($this->_input, $offset, $p - $offset);
                $this->_output .= "<?php " . substr($this->_input, $p + 2, $end - $p - 2) . " ?>";
                $offset = $end + 1;
                continue;
            }

            // replace HTML-encoded statements @:...
            if ($this->isNextToken($p, $endBlock, ":"))
            {
                $statement = $this->detectStatement($p + 2, $endBlock);
                $end = $this->findEndStatement($p + 1 + strlen($statement), $endBlock);
                $this->_output .= substr($this->_input, $offset, $p - $offset);
                $this->generateOutput($p + 1, $end, true);
                $offset = $end + 1;
                continue;
            }

            // replace translate statements @+...
            if ($this->isNextToken($p, $endBlock, "+"))
            {
                $statement = $this->detectStatement($p + 2, $endBlock);
                $end = $this->findEndStatement($p + 1 + strlen($statement), $endBlock);
                $this->_output .= substr($this->_input, $offset, $p - $offset);
                $this->generateTranslateOutput($p + 1, $end, true);
                $offset = $end + 1;
                continue;
            }

            $statement = $this->detectStatement($p + 1, $endBlock);
            if ($statement == "foreach" || $statement == "for" || $statement == "while")
            {
                $offset = $this->processLoopStatement($p, $offset, $endBlock, $statement);
            }
            else if ($statement == "if")
            {
                $offset = $this->processIfStatement($p, $offset, $endBlock, $statement);
            }
            else
            {
                $end = $this->findEndStatement($p + strlen($statement), $endBlock);
                $this->_output .= substr($this->_input, $offset, $p - $offset);
                $this->generateOutput($p, $end);
                $offset = $end + 1;
            }
        }

        $this->_output .= substr($this->_input, $offset, $endBlock - $offset);
    }

    /**
     * Generates a scaped php output.
     *
     * @param mixed $currentPosition
     * @param mixed $endPosition
     * @param mixed $htmlEncode
     */
    private function generateOutput($currentPosition, $endPosition, $htmlEncode = false)
    {
        $this->_output .= "<?php echo " . ($htmlEncode ? "htmlspecialchars(" : "") . substr($this->_input, $currentPosition + 1, $endPosition - $currentPosition) . ($htmlEncode ? ")" : "") . "; ?>";
    }

    /**
     * Generates a scaped php output.
     *
     * @param mixed $currentPosition
     * @param mixed $endPosition
     * @param mixed $htmlEncode
     */
    private function generateTranslateOutput($currentPosition, $endPosition)
    {
        $this->_output .= "<?php echo \$this->translate('" . substr($this->_input, $currentPosition + 1, $endPosition - $currentPosition) . "'); ?>";
    }

    /**
     * Processes a loop statement.
     *
     * @param mixed $currentPosition
     * @param mixed $offset
     * @param mixed $endBlock
     * @param mixed $statement
     */
    private function processLoopStatement($currentPosition, $offset, $endBlock, $statement)
    {
        if (($bracketPosition = $this->findOpenBracketAtLine($currentPosition + 1, $endBlock)) === false)
        {
            throw new Svc_Exception (Svc_Exception::TEMPLATE_VIEW_SYNTAX_ERROR, "Cannot find open bracket for '{$statement}' statement - " . $this->_file . " at line " . $this->getLineNumber($currentPosition));
        }

        $this->_output .= substr($this->_input, $offset, $currentPosition - $offset);
        $this->_output .= "<?php " . substr($this->_input, $currentPosition + 1, $bracketPosition - $currentPosition) . " ?>";
        $offset = $bracketPosition + 1;

        $end = $this->findClosingBracket($bracketPosition, $endBlock, "{", "}");
        $this->parse($offset, $end);
        $this->_output .= "<?php } ?>";

        return $end + 1;
    }

    /**
     * Processes an "if" statement.
     *
     * @param mixed $currentPosition
     * @param mixed $offset
     * @param mixed $endBlock
     * @param mixed $statement
     */
    private function processIfStatement($currentPosition, $offset, $endBlock, $statement)
    {
        $bracketPosition = $this->findOpenBracketAtLine($currentPosition + 1, $endBlock);
        if ($bracketPosition === false)
        {
            throw new Svc_Exception (Svc_Exception::TEMPLATE_VIEW_SYNTAX_ERROR, "Cannot find open bracket for '{$statement}' statement - " . $this->_file . " at line " . $this->getLineNumber($currentPosition));
        }

        $this->_output .= substr($this->_input, $offset, $currentPosition - $offset);
        $start = $currentPosition + 1;
        while (true)
        {
            $this->_output .= "<?php " . substr($this->_input, $start, $bracketPosition - $start + 1) . " ?>";
            $offset = $bracketPosition + 1;

            $end = $this->findClosingBracket($bracketPosition, $endBlock, "{", "}");
            $this->parse($offset, $end);
            $offset = $end + 1;

            $bracketPosition = $this->findOpenBracketAtLine($offset, $endBlock);
            if ($bracketPosition === false)
            {
                $this->_output .= "<?php } ?>";
                break;
            }

            $start = $end;
        }

        return $offset;
    }

    /**
     * Finds the open bracket inside the current line.
     *
     * @param mixed $currentPosition
     * @param mixed $endBlock
     */
    private function findOpenBracketAtLine($currentPosition, $endBlock)
    {
        $openDoubleQuotes = false;
        $openSingleQuotes = false;

        for ($p = $currentPosition; $p < $endBlock; ++$p)
        {
            if ($this->_input[$p] == "\n")
            {
                return false;
            }

            $quotesNotOpened = !$openDoubleQuotes && !$openSingleQuotes;
            if ($this->_input[$p] == '"')
            {
                $openDoubleQuotes = $this->getQuotesState($openDoubleQuotes, $quotesNotOpened, $p);
            }
            else if ($this->_input[$p] == "'")
            {
                $openSingleQuotes = $this->getQuotesState($openSingleQuotes, $quotesNotOpened, $p);
            }
            else if ($this->_input[$p] == "{" && $quotesNotOpened)
            {
                return $p;
            }
        }

        return false;
    }

    /**
     * Checks the next token.
     *
     * @param mixed $currentPosition
     * @param mixed $endBlock
     * @param mixed $token
     */
    private function isNextToken($currentPosition, $endBlock, $token)
    {
        return ($currentPosition + strlen($token)) < $endBlock && substr($this->_input, $currentPosition + 1, strlen($token)) == $token;
    }

    /**
     * Checks if a string is scaped.
     *
     * @param mixed $currentPosition
     */
    private function isEscaped($currentPosition)
    {
        $cntBackSlashes = 0;
        for ($p = $currentPosition - 1; $p >= 0; --$p)
        {
            if ($this->_input[$p] != "\\")
            {
                break;
            }
            ++$cntBackSlashes;
        }

        return $cntBackSlashes % 2 == 1;
    }

    /**
     * Gets the current quote state.
     *
     * @param mixed $testedQuotes
     * @param mixed $quotesNotOpened
     * @param mixed $currentPosition
     */
    private function getQuotesState($testedQuotes, $quotesNotOpened, $currentPosition)
    {
        if ($quotesNotOpened)
        {
            return true;
        }

        return $testedQuotes && !$this->isEscaped($currentPosition) ? false : $testedQuotes;
    }

    /**
     * Finds the next closing bracket.
     *
     * @param mixed $openBracketPosition
     * @param mixed $endBlock
     * @param mixed $openBracket
     * @param mixed $closeBracket
     */
    private function findClosingBracket($openBracketPosition, $endBlock, $openBracket, $closeBracket)
    {
        $opened = 0;
        $openDoubleQuotes = false;
        $openSingleQuotes = false;

        for ($p = $openBracketPosition; $p < $endBlock; ++$p)
        {
            $quotesNotOpened = !$openDoubleQuotes && !$openSingleQuotes;

            if ($this->_input[$p] == '"')
            {
                $openDoubleQuotes = $this->getQuotesState($openDoubleQuotes, $quotesNotOpened, $p);
            }
            elseif ($this->_input[$p] == "'")
            {
                $openSingleQuotes = $this->getQuotesState($openSingleQuotes, $quotesNotOpened, $p);
            }
            elseif ($this->_input[$p] == $openBracket && $quotesNotOpened)
            {
                $opened++;
            }
            elseif ($this->_input[$p] == $closeBracket && $quotesNotOpened)
            {
                if (--$opened == 0)
                {
                    return $p;
                }
            }
        }

        throw new Svc_Exception (Svc_Exception::TEMPLATE_VIEW_SYNTAX_ERROR, "Cannot find closing bracket - " . $this->_file . " at line " . $this->getLineNumber($openBracketPosition));
    }

    /**
     * Finds the ending statement.
     *
     * @param mixed $endPosition
     * @param mixed $endBlock
     */
    private function findEndStatement($endPosition, $endBlock)
    {
        if ($this->isNextToken($endPosition, $endBlock, "("))
        {
            $endPosition = $this->findClosingBracket($endPosition + 1, $endBlock, "(", ")");
            $endPosition = $this->findEndStatement($endPosition, $endBlock);
        }
        else if ($this->isNextToken($endPosition, $endBlock, "["))
        {
            $endPosition = $this->findClosingBracket($endPosition + 1, $endBlock, "[", "]");
            $endPosition = $this->findEndStatement($endPosition, $endBlock);
        }
        else if ($this->isNextToken($endPosition, $endBlock, "->"))
        {
            $endPosition += 2;
            $statement = $this->detectStatement($endPosition + 1, $endBlock);
            $endPosition = $this->findEndStatement($endPosition + strlen($statement), $endBlock);
        }
        else if ($this->isNextToken($endPosition, $endBlock, "::"))
        {
            $endPosition += 2;
            $statement = $this->detectStatement($endPosition + 1, $endBlock);
            $endPosition = $this->findEndStatement($endPosition + strlen($statement), $endBlock);
        }

        return $endPosition;
    }

    /**
     * Detects the current statement.
     *
     * @param mixed $currentPosition
     * @param mixed $endBlock
     *
     * @return string
     */
    private function detectStatement($currentPosition, $endBlock)
    {
        $invalidCharPosition = $endBlock;

        for ($p = $currentPosition; $p < $invalidCharPosition; ++$p)
        {
            if ($this->_input[$p] == "$" && $p == $currentPosition)
            {
                continue;
            }

            if (preg_match('/[a-zA-Z0-9_]/', $this->_input[$p]))
            {
                continue;
            }

            $invalidCharPosition = $p;
            break;
        }

        if ($currentPosition == $invalidCharPosition)
        {
            throw new Svc_Exception (Svc_Exception::TEMPLATE_VIEW_SYNTAX_ERROR, "Cannot detect statement " . $this->_file . " at line " . $this->getLineNumber($currentPosition));
        }

        return substr($this->_input, $currentPosition, $invalidCharPosition - $currentPosition);
    }

    /**
     * Gets the line number.
     *
     * @param mixed $currentPosition
     *
     * @return int
     */
    private function getLineNumber($currentPosition)
    {
        return count(explode("\n", substr($this->_input, 0, $currentPosition)));
    }

}
